State Object Controller topic
When first learning Flutter, you've likely encountered one particular warning message over and over again (see below). That's because you're not taking full advantage of Flutter’s declarative approach to programming. In the screenshot, the keyword, final, was removed from the instance variable, title, allowing that variable's value to possibly be 'changed' during the runtime of that particular StatefulWidget.
State | Control | Example | Set Control | Functions | Events | Which State | Context | App |
It's Inevitably Immutable
Your app's StatelessWidgets and StatefulWidgets are to contain unchanging (immutable) instance fields and properties. If not, you're actually impeding Flutter’s general functionality and degrading its overall performance. The less that changes, the better. Use the keyword, final, for variables assigned only once. Better still, use the keyword, const, if you know the property's value before compile-time will never change. As your app runs, Flutter is calling those widgets over and over again.
“There is no imperative changing of the UI itself (like widget.setText)—you change the state,
and the UI rebuilds from scratch.”
— flutter.dev Start thinking declaratively
Now, I suspect in the beginning, all your ‘mutable’ code was going into your State class. A reasonable idea at first glance. After all, you always want ready access to the State object and its setState() function. However, that can make for a rather large and unmanageable Dart file placing all the business logic and such under one State class. I quickly found, more often than not, placing such code in a separate Dart file in a separate class made for a better approach. You're explicitly separating the app's interface from its business rules. Looking how Flutter implemented such an approach already using Controllers in widgets, I created the 'State Object Controller' class.
Again, however, when working with Flutter, you always need reliable access to a particular State object so to call its setState() function (other developer's choose instead the StatefulElement’s markNeedsBuild() function). Doing so ‘rebuilds’ that portion of the screen involving that State object and reflects the changes made. That means the State Object controller would need access to its designated State object.
State of Control
Through the course of an app’s lifecycle, a controller can be assigned to any number of StateX objects. A StateXController object works with ‘the last’ State object it’s been assigned to but keeps a reference of any and all State objects it’s previously worked with in the Widget tree. When a screen closes (i.e. the current StateX object is disposed of), the controller returns back to the previous StateX it was assigned to. This allows, for example, for one controller to sustain the app’s business rules for the duration of the running app conveying that logic across any number of screens (i.e. any number of StateX objects).
Control The State
In turn, the StateX object can take in any number of controllers. You then delegate controllers to specific areas of responsibility. Each can be independent of the other encouraging modular development. The State object's build() function produces the interface while its controllers are concerned with everything else.
Show By Example
Here is a gist file for you to download and follow along: statex_counter_app.dart
As you see in the first screenshot below, the traditional State class has been replaced with the class, StateX,
and takes in a StateXController object named, YourController.
In the next screenshot below,
you can see highlighted with red arrows that the Controller object is referenced here and there in the State object’s build() function.
This is by design.
The controller is providing the data and the event handling necessary for the app to function properly.
However, note there’s no setState() function call.
It’s in the controller and called in the con.onPressed() function.
statex_counter_app.dart | statex_counter_app.dart |
---|
Set Control
Looking at the first screenshot of the controller class below, you’ll find the setState() function call. Now, that’s a powerful capability! You’re in a class that can contain all the mutable properties and business logic you want as well as provide state management! It further has access to the State object’s own properties: widget, mounted, and context. Imagine what you’re controller can do with access to those properties as well. The second screenshot is a quick peek into the StateX class itself confirming the Controller is linked to the State object is some fashion.
statex_counter_app.dart | state_extended.dart |
---|
As an aside, if you don’t want the ‘business side’ of your app dictating the ‘look and feel’ of your app’s interface, simply remove the setState() function call from the controller, and return it back to the build() function (see below). Let the State object dictate when to call its setState() function instead. With the use of such controllers, you have options. The second screenshot below of our simple example app demonstrates how some additional controller objects can be linked to a StateX object.
statex_counter_app.dart | statex_counter_app.dart |
---|
Functions and Features
Listed below are the additional functions and features you gain with the StateXController class:
/// The current StateX object
StateX? get state
/// Associate this StateXController to the specified State object
/// Returns that State object's unique identifier
String addState(StateX? state)
/// Calls the ‘current’ StateX object’s setState() function
void setState(VoidCallback fn)
/// Retrieve the State object by its StatefulWidget
/// Returns null if not found
StateX? stateOf<T extends StatefulWidget>()
/// Retrieve the StateX object by type
/// Returns null if not found
T? ofState<T extends StateX>()
Control Events
Lastly, there are some twenty-two (22) event handlers available to a controller when taken in by its StateX object. For example, after running its own initState() function, a StateX object will then run the initState() function of each and every controller currently 'associated' with it at that moment. You see, each controller may have its own operations that need to run before the StateX object can continue. Instead of one large messy initState() function in the one State object, there can be individual controllers running their own initState() functions. Very modular. Very clean. This process comes about with the other twenty-one system events as well.
/// Called exactly once when the State object is first created
void initState()
/// Called exactly once at the app’s startup to initialize any ‘time-consuming’ operations
/// that need to complete for the app can continue
Future<bool> initAsync()
/// When the State object will never build again. Its terminated
void dispose()
/// Override this method to respond when the State object’s accompanying StatefulWidget is destroyed
and a new one recreated
/// — a very common occurrence in the life of a typical Flutter app
void didUpdateWidget(StatefulWidget oldWidget)
/// When a dependency of this State object changes
void didChangeDependencies()
/// Brightness changed
void didChangePlatformBrightness()
/// When the user’s locale has changed
void didChangeLocale(Locale locale)
/// When the application’s dimensions change. (i.e. when a phone is rotated.)
void didChangeMetrics()
/// Called during hot reload. (e.g. reassembled during debugging.)
void reassemble()
/// Called when the system tells the app to pop the current route
void didPopRoute()
/// Called when the app pushes a new route onto the navigator
void didPushRoute(String route)
/// Called when pushing a new RouteInformation and a restoration state
/// onto the router
void didPushRouteInformation(RouteInformation routeInformation)
/// When the State object is removed from the Widget tree
/// Best to close things up in this function and not the dispose() function
/// Like garbage collecting, the dispose() function call is to the discretion of the OS
void deactivate()
/// When the platform’s text scale factor changes
void didChangeTextScaleFactor()
/// When the phone is running low on memory
void didHaveMemoryPressure()
/// When the system changes the set of active accessibility features
void didChangeAccessibilityFeatures()
/// Called when the app’s in the background or returns to the foreground
/// The four following functions use this one to address specific events
void didChangeAppLifecycleState(AppLifecycleState state)
/// The application is in an inactive state and is not receiving user input
void inactiveLifecycleState()
/// The application is not currently visible to the user, not responding to
/// user input, and running in the background
void pausedLifecycleState()
/// Either be in the progress of attaching when the engine is first initializing
/// or after the view being destroyed due to a Navigator pop
void detachedLifecycleState()
/// The application is visible and responding to user input
void resumedLifecycleState()
Control Which State
Let's continue by highlighting some of the functions listed above. As always, the example app (depicted in video) is a great resource for you to fully understand what this StateX package can do for you. The sample of code below demonstrates the ready access you have to any particular State object used by a controller. It's showing how you can increment the counter displayed in Page 1 from Page 2 of a 3-Page counter app. You can see how one is given vital access to the appropriate setState() function from outside the State object you're working with!
page_02.dart |
---|
Control Context
The screenshot above is of the State class, Page2State. Let's take a quick peek of its initState() function displaying for demonstration purposes the properties also available to a controller once associated with a State object. Note, even if the controller as not 'added' to a particular State object, some of these properties are still viable. For example, in the first screenshot below, every controller has a rootState property returning the first State object for the app. In the second screenshot highlighted are additional properties that provide values whether the controller has been assigned a State object or not. The latest context object is available to you in a number of ways using a controller. A 'data object' initially supplied to your app is available to every controller. It's an object and so can be anything you imagine and or required by available throughout your app. Lastly, there a bool property indicating whether the app is running in production or not ---a useful indicator during development.
page_02.dart | page_02.dart |
---|
Control The App
The example app, appstatex_example_app, was first introduced in the topic, AppStateX class. Let's review the use of controllers in this app, and see how they're involvement only makes for better code. The first screenshot below is of the App State class, _MyAppState. At a glance, you can see what controllers are involved in this app, and maybe even deduce their individual roles. Those few actually taken in by the State class, as you know, will have ready access to that State class; its properties and functions. Any event functions those controllers may have will be run by the State object at the appropriate time and circumstance.
In the first screenshot below, we see the instance variable, app, is assigned to the controller, MyAppController. You can assume this is the State object's primary controller responsible for or at least involved in the app's overall functionality. In the second screenshot, you see the StateX object's controllerByType() function is used to retrieve an instance of the controller class, GoogleFontController. Since it was taken in by the State object earlier, this is one means to retrieve that instance. Note, the second line there is merely to demonstrate an alternate approach available to you. Since, the GoogleFontController class will only ever have one instance because of its factory constructor, it's just as acceptable to call its constructor again and again to instead retrieve that one instance.
myapp_view.dart | home_page.dart |
---|
Again, in this example app, the controller, MyAppController, directs the general functionality of the app. An example of this is demonstrated in the first screenshot below. The App State class, _MyAppState will either use the MaterialApp widget or the CupertinoApp widget depending on the value of its controller's useMaterial property. If the Material design is used, the second screenshot below demonstrates the continued contribution of the State object's controllers. You can readily see the MyAppController controller also provides the popup menu and the drawer for the Material interface. Note, however, the MyHomePageController controller is responsible for what happens when the FloatingActionButton widget is tapped. At a glance, you can see the separation of responsibility: different controllers for different responsibilities. The MyHomePageController controller is involved with the count and has the appropriate method called, onPressed.
myapp_view.dart | home_page.dart |
---|
The ColorPickerController class is also a controller. In other words, it extends the class, StateXController. However, in this example app, it is never taken in by a State object. If that's the case, why be a controller? The first screenshot conveys why. A StateXController has properties and functions that are still available even when not taken in by a State object.
color_picker_controller.dart |
---|
Classes
- StateXController Get started State Object Controller Testing Event handling
- Your 'working' class most concerned with the app's functionality. Add it to a 'StateX' object to associate it with that State object.
- Uuid StateX class State Object Controller
- Shamelessly extracted from the author of Scoped Model plugin, Who maybe took from the Flutter source code. I'm not telling!
Mixins
- AsyncOps StateX class State Object Controller
- Supply the Async API
- RootState StateX class State Object Controller
- Supply access to the 'Root' State object.
- SetStateMixin State Object Controller
- Used by StateXController Allows you to call 'setState' from the 'current' the State object.
- StateListener StateX class State Object Controller Event handling
- Responsible for the event handling in all the Controllers and State objects.